package br.gfca.simplenet;

import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

import br.gfca.simplenet.util.Printer;

public class ServerImpl {
	
	private ServerListener listener;
	
	private ServerSocket serverSocket;
	private Socket[] sockets;
	private ObjectOutputStream[] outs;
	private ObjectInputStream[] ins;
	
	private int port;
	private boolean accepting;
	
	public ServerImpl( int port, int maxClients, ServerListener listener ) {
		this.port = port;
		this.listener = listener;
		this.initArrays( maxClients );
		this.accepting = false;
	}
	
	public void waitConnections() {
		
		if ( isAccepting() ) {
			Printer.getInstance().printError("Attempt to wait connections on an already startet server socket.");
			return;
		}
		
		Thread t = new Thread( new Runnable() {
			@Override
			public void run() {
				try {
					serverSocket = new ServerSocket( port );
				}
				catch (SecurityException se) {
					this.stopServerOnError("Failed to create server socket:", se);
					return;
				}
				catch (IOException ioe) {
					this.stopServerOnError("Failed to create server socket:", ioe);
					return;
				}
				
				while ( !maxClientsReached() ) {
					Socket socket = null;
					try {
						accepting = true;
						socket = serverSocket.accept();
						int id = setNewSocket( socket );
						listener.clientConnected( id );
						beginAcceptMessages( id );
					}
					catch (SocketException se) {
						// Server has been stopped and server socket closed.
						// There is no need to alert listener.
						accepting = false;
						return;
					}
					catch (RuntimeException re) {
						this.stopServerOnError("Error while waiting connections:", re);
						return;
					}
					catch (IOException ioe) {
						this.stopServerOnError("Error while waiting connections:", ioe);
						return;
					}
					catch (UnavailableSlotException use) {
						Printer.getInstance().printError("Unable to assign new socket to a free slot:");
						Printer.getInstance().printStack( use );
						try {
							if (socket != null ) {
								socket.close();
							}
						} catch (IOException e) {
							socket = null;
						}
					} catch (StreamCreationException sce) {
						Printer.getInstance().printError("Error while creating io strems for socket:");
						Printer.getInstance().printStack( sce );
						try {
							if (socket != null ) {
								socket.close();
							}
						} catch (IOException e) {
							socket = null;
						}
					}
				}
				listener.maxClientsReached( getMaxUsers() );
				closeServerSocket();
				accepting = false;
			}
			
			private void stopServerOnError( String debugMessage, Exception e ) {
				Printer.getInstance().printError(debugMessage);
				Printer.getInstance().printStack( e );
				
				closeServer();
				accepting = false;
				
				listener.waitingFailed( new WaitingFailureEvent( e.getMessage() ) );
			}
		});
		
		t.start();
	}
	
	public void sendMessage( final String message, final int clientId ) {
		new Thread( new Runnable() {
			@Override
			public void run() {
				if ( isConnected( clientId ) ) {
					try {
						outs[ clientId ].writeUTF( message );
						outs[ clientId ].flush();
						listener.messageSent( clientId, message );
					}
					catch (IOException ioe) {
						Printer.getInstance().printError("Failed to send message:");
						Printer.getInstance().printStack( ioe );
						listener.messageFailed( clientId, message );
					}
				}	
			}			
		}).start();	
	}

	public void broadcastMessage(String message, int exceptSender) {
		// there is no exception, send to all clients
		if (exceptSender < 0) {
			for (int i = 0; i < this.sockets.length; i++) {
				this.sendMessage(message, i);
			}
		}
		// do not send to the original sender
		else {
			for (int i = 0; i < this.sockets.length; i++) {
				if ( i != exceptSender ) {
					this.sendMessage(message, i);
				}				
			}
		}				
	}

	public void disconnectClient(int id) {
		if ( this.isConnected( id ) ) {
			try {
				this.sockets[id].close();
			}
			catch (IOException ioe) {
				Printer.getInstance().printError("Failed to close client socket id " + id + ":");
				Printer.getInstance().printStack( ioe );
			}
		}
		this.sockets[id] = null;
		this.outs[id] = null;
		this.ins[id] = null;
	}

	public void closeServer() {
		this.closeServerSocket();
		this.closeClientSockets();
	}

	public boolean isAccepting() {
		return accepting;
	}

	private void beginAcceptMessages( final int clientId ) {
		Thread t = new Thread( new Runnable() {
			@Override
			public void run() {
				while ( isConnected( clientId ) ) {
					try {
						String message = ins[ clientId ].readUTF();
						listener.messageReceived( message, clientId );
					}
					catch (SocketException se) {
						// Server has been stopped and the socket was closed.
						// There is no need to alert listener.
						Printer.getInstance().printError("Client socket has been closed due to server stop:");
						Printer.getInstance().printStack( se );
						return;
					}
					catch (EOFException eofe) {
						// Socket has been closed by client (client disconnection)
						// Alert listener of client disconnection and remove socket
						Printer.getInstance().printError("Socket has been closed by client:");
						Printer.getInstance().printStack( eofe );
						disconnectClient( clientId );
						listener.clientDisconnected( clientId );
						return;
					}
					catch (IOException ioe) {
						// Unknown input error. Socket will be disconnected
						// and listener will be warned.
						Printer.getInstance().printError("Unknown input error. Socket will be disconnected:");
						Printer.getInstance().printStack( ioe );
						disconnectClient( clientId );
						listener.clientDisconnected( clientId );
						return;
					}					
				}
			}
		});
		t.start();
	}

	private int setNewSocket(Socket s) throws UnavailableSlotException, StreamCreationException {
		int id = this.findAvailableId();
		if ( id < 0 ) {
			// Unable to assign the new socket to a slot
			throw new UnavailableSlotException("There is no available slot.");
		}
		else {
			try {
				// acquire streams
				this.sockets[id] = s;
				this.outs[id] = new ObjectOutputStream( s.getOutputStream() );
				this.ins[id] = new ObjectInputStream( s.getInputStream() );
				return id;
			} catch (Exception e) {
				// without streams the socket is useless
				this.sockets[id] = null;
				this.outs[id] = null;
				this.ins[id] = null;
				throw new StreamCreationException("Error creating streams.", e);
			}			
		}
	}

	private void initArrays( int maxClients ) {
		this.sockets = new Socket[ maxClients ];
		this.outs = new ObjectOutputStream[ maxClients ];
		this.ins = new ObjectInputStream[ maxClients ];
	}

	private int findAvailableId() {
		for ( int i = 0; i < this.sockets.length; i++ ) {
			if ( this.sockets[i] == null ) {
				return i;
			}
		}
		return -1;
	}

	private int getMaxUsers() {
		return this.sockets != null ? this.sockets.length : 0;
	}

	private void closeServerSocket() {
		if (this.serverSocket != null && !this.serverSocket.isClosed()) {
			try {
				this.serverSocket.close();
			}
			catch (IOException ioe) {
				Printer.getInstance().printError("Error while closing server socket:");
				Printer.getInstance().printStack( ioe );
			}
		}
		this.serverSocket = null;
	}

	private void closeClientSockets() {
		for (int s = 0; s < this.sockets.length; s++) {
			this.disconnectClient( s );
		}
	}

	private boolean isConnected(int clientId) {
		return this.sockets[clientId] != null && this.sockets[clientId].isConnected();
	}

	private boolean maxClientsReached() {
		for ( Socket s : this.sockets ) {
			if ( s == null ) {
				return false;
			}
		}
		return true;
	}	
}